消息机制、事件分发、滑动冲突

浅析消息机制、事件分发、滑动冲突

消息机制

大致流程

  1. 启动应用程序的同时,主线程会调用Looper.prepareMainLooper()方法,初始化Looper对象(在这个Looper对象内部会维护者MessageQueue) (ActivityThread.java的6642)

  2. 当调用handler.sendMessage()发消息的时候,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条 消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //handler
    //746
    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
    long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
    msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
    }
  3. 主线程调用Looper.loop()开启循环,然后论询消息队列,通过MessageQueue.next()去取出消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //Looper.loop()   151行

    //152 获取ThreadLocal中储存的Looper对象
    final Looper me = myLooper();
    //156 获取Looper对象的消息队列
    final MessageQueue queue = me.mQueue;
    //173 进入looper的主循环(永真循环)
    //当next()读了下一条消息以后,消息队列为空了,next()会产生无线循环。
    for (;;) {
    //在这个next内部也有一个永真循环。next调用到最后调用到#Looper.cpp::pollInner()
    Message msg = queue.next(); // might block
    if (msg == null) {
    // No message indicates that the message queue is quitting.
    return;
    }
    .......
    }

    //214 获取msg的目标Handler,然后分发Message
    msg.target.dispatchMessage(msg);

    ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量可以在不同的线程中互不干扰地储存并提供数据。

    关于为什么最后它不会卡死,原因就是它没有很高频率地占用CPU。当queue为空时。next()通过nativePollOnce()这个navtive方法造成了阻塞,阻塞最终是通过epoll机制实现,当我们向消息队列发送事件时,最终会间接向管道的“写入端”写入数据,于是epoll通过管道的“读取端”立即就感知到了,epoll_wait()在等到事件后,随即进行相应的事件处理。

    其实这里的死循环也是为了保证ActivityThread一直活着。ActivityThread本身是运行在主线程之上,但是它并非是真正的继承了Thread类,至于为什么有死循环又能够处理消息,这是因为ActivityThread有一个内部类H, 其继承于Handler,这个H会根据不同的msg来执行相应的东西(比如声明周期)

  4. 取出的meeeage不为空就调用msg.target.dispatchMessage()分发消息,目标handler收到消息以后会执行handler.handlerMessage()方法处理消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //Handler
    //194 关联当前线程的Looper ,并且这个looper不能为空
    mLooper = Looper.myLooper();
    if (mLooper == null) {
    throw new RuntimeException(
    "Can't create handler inside thread " + Thread.currentThread()
    + " that has not called Looper.prepare()");
    }
    //210 将looper的MessageQueue作为自己的MessageQueue
    //所以它的消息都会发送并关联到looper的MessageQueue上
    mQueue = mLooper.mQueue;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //Handler
    //98
    public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) { //这里的就是执行回调函数,最开始是调用handler.post()
    handleCallback(msg);
    } else {
    //如果自己实现了Handler.Callback接口情况
    if (mCallback != null) {
    if (mCallback.handleMessage(msg)) {
    return;
    }
    }
    handleMessage(msg);
    }
    }

    //695
    //Handler的sendxxx方法还有post方法最后都是调用
    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis)
1
2
3
4
5
6
//MessageQueue
//352 定时执行
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}

重点核心

  1. Handler可以在任意线程中发送消息,这些消息会被添加到关联的MessageQueue上
  2. Handler是在它关联的Looper线程(即Looper绑定的线程)处理消息
  3. Android的主线程也是一个Looper线程

关于Hadler的内存泄漏

原因

handler发送的消息在当前handler的消息队列中,如果此时activity finish掉了,那么消息队列的消息依旧会由handler进行处理,若此时handler声明为内部类(非静态内部类),我们知道内部类天然持有外部类的实例引用,那么就会导致activity无法回收,进而导致activity泄露。

处理

  • 保证activity被销毁的时候,该线程的消息队列没有这个Activity的handler内部类的引用
  • 让这个handler不持有Activity等外部组件实例
  • 将Handler放到一个单独的顶层类文件中

事件分发

什么是事件分发

口述(好吧,其实是我懒得打字,hhh)

事件本身

分发的是啥?毕竟我们是面向对象的,所以,分发的当然是对象,这些对向都是用户触摸屏幕而产生的一系列事件,事件主要包括按下、滑动、抬起等。这些事件都被封装成MotionEvent对象

1
2
3
4
5
MotionEvent.ACTION_POINTER_DOWN    //在屏幕上按下

MotionEvent.ACTION_MOVE //在屏幕上移动

MotionEvent.ACTION_UP //抬起手指,这个是所有的正常点击事件的正常结束的动作
  • 按下、滑动、抬起等事件组成了一个事件流。事件流以按下开始,中间可能由若干次滑动,以抬起或取消作为结束
  • 在Android的事件分发的处理过程中,主要是寻找能够处理 按下事件 的组件
  • 从按下事件开始,按照默认流程走了一遍,直到回到Activity的onTouchEvent()方法,这个方法在默认情况下依然会返回false,那就表示没有任何一部分会处理这个事件列,那么这个事件列的后续部分将不会被接受

事件分发浅析

  • activity

    1
    2
    3
    4
    5
    6
    7
    8
    //Activity的核心伪代码
    public boolean dispatchTouchEvent(MotionEvent ev) {
    if (child.dispatchTouchEvent(ev)) { //先看子view是否消费事件
    return true;
    } else {
    return onTouchEvent(ev); //如果子View没有消费该事件,则调用自身的onTouchEvent()处理。
    }
    }
  • viewgroup

    1
    2
    3
    4
    5
    6
    7
    8
    //ViewGroup的核心伪代码
    public boolean dispatchTouchEvent(MotionEvent ev) {
    if (!onInterceptTouchEvent(ev)) {
    return child.dispatchTouchEvent(ev); //不拦截,则传给子View进行分发处理
    } else {
    return onTouchEvent(ev); //拦截事件,交由自身对象的onTouchEvent()方法处理
    }
    }
    • onInterceptTouchEvent()如果对某个事件拦截以后,这个事件列后面的事件将会直接交给onTouchEvent()处理,后续事件不再进行判断
  • view

    1
    2
    3
    4
    5
    6
    7
    8
    //View的核心伪代码
    public boolean dispatchTouchEvent(MotionEvent ev) {
    //如果该对象的监听成员变量不为空,则会调用其onTouch方法,
    if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
    return true; //若onTouch()方法返回TRUE,则表示消费了该事件,dispachtouTouchEvent返回true
    }
    return onTouchEvent(ev); //若监听成员为空或onTouch()没有消费该事件,则调用对象自身的onTouchEvent()方法处理。
    }
    • 如果OntouchListener的onTouch()直接返回了true,那么onTouchEvent()将被跳过
    • 在view的onTouchEvent()中,如果设置了onClickListener监听,就会调用其onClick()方法
    • 在调用到dispatchTouchEvent()时,会先判断该view是否可见,不可见则直接返回false
    • 可以使用requestDisallowInterceptTouchEvent()来使ViewGroup不调用onInterceptTouchEvent()而直接将事件分发给子View

几点不知道咋归类的东西

  • dispatchTouchEvent()中返回true,只拦截当前ACTION_DOWN时间,返回的true或者false不影响后续其他事件的传递

  • onTouchEvent()中返回true时,后续的其他事件全都会被拦截

流程图

滑动冲突

什么是滑动冲突

我们来看一看实际案例(图是偷的),请手动把url放到浏览器中。谢谢

https://upload-images.jianshu.io/upload_images/8669504-8a1549edcfb10af7.gif?imageMogr2/auto-orient/strip|imageView2/2/w/270/format/webp

产生的场景

  • 父ViewGroup和子View的滑动方向一致

    如果父VIewGroup和子View的滑动方向一致,如果我们需要让两者都滑动。当ViewGroup接收到事件以后,由于不会拦截事件,就会将事件传给子View,一旦有子View处理了这个ACTION_DOWN事件,在默认情况下,这个事件列的后续事件都将交给这个子View处理,这个时候由于子View是可以滑动的,但是父ViewGroup始终滑动不了

  • 父ViewGroup和子View的滑动方向不一致

解决

  • 外部拦截

    可以通过ViewGroup的onInterceptTouchEvent()方法进行判断,然后决定是否拦截(详见demo代码讲解)

  • 内部拦截

    可以通过getParent().requestDisallowInterceptTouchEvent(),请求父容器拦截或者不拦截事件可以在子view的dispatchTouchEvent()或者onTouchEvent()中进行操作

更复杂的情况

如果产品提出了更复杂的情况的话,最好的解决办法是给产品看看你抽屉里的菜刀,应该问题自动就解决了(开个玩笑)。遇到很复杂的情况的话,大家就得自己想想办法了,我举的栗子都是很简单、基本、常见的。但是只要抓住了根本原因的话。问题不是很大